Reversing - xor0_crackme_1
In a previous article I described the process of reverse-engineering a crackme. These are programs designed to be broken with various counter-mesaures and challenges. As a break from my hardware work, I thought I would have a go at another one.
Crackme
The crackme in question is this one, the newest available on the site at the time. If you want to give it a go, you should stop reading since there are major spoilers ahead! I’ll try to write keygen(s) where suitable, or at least gain enough knowledge to pass the checks and be able to write a keygen.
Initial analysis shows a unobsfucated C# program, that calls into helper.dll in order to produce the result.
The dll is a native one. After finding the exported function, we can see it is calling into the windows api to get the current time. Then there is a switch on the day of the week, where each day calls a different function with the passed name
and serial
arguments.
(in the picture, I have patched the program, hardcoding the day whilst I am working on it. The original code used the day of week from the SYSTEMTIME struct returned frm the GetLocalTime API call.)
Sunday
Let’s look at Sunday’s function
We can see here it passes the serial along to some other function, checks the result is 0x13, and if it is, it then checks each byte matches a hard-coded serial of A10-57617274-686F76
. The other function is almost certainly some kind of strlen, then, since the serial is 0x13 characters long:
Indeed it is a typical implementation of strlen, which I have labelled as such since it is likely going to turn up again yet.
From this we can deduce it doesn’t matter what the name
parameter is, and the serial is hardcoded to A10-57617274-686F76
Monday
Monday’s function first tries to load a file “xor0.rox” and checks that it is exactly 0x20 bytes long. If so it then reads the contents into a buffer in memory. This appears to be some kind of license style file.
Here it XORs the 4th character of the user name with the 5th character of the serial number, and depending on the result, might add 1 to it. Let’s call this the magic number. It then checks if this number appears at the 5th byte of the loaded file (the address 1000E384
is the buffer + 4).
This code checks that the first four bytes are equal to 0xDEADC0DE.
Therefore our little-endian license file currently looks like this, based on a username of name12345
and serial serial
.
DEC0ADDE04 (+ 0x1b more bytes)
The last part is quite a lot more tricky. The code here basically sums the remaining 0x1b bytes, failing if any of them are zero. It then multiplies the result with the magic number, and XORs the result with the last 32 bits in the file. The result must produce 0xFACE0FB0
.
I’m sure there’s a nice maths way to work out the possible solutions - I decided to work backwards until I can produce a working license file for my name and serial combination, based on the following observations:
- No bytes can be zero
- The sum, before multiplication, includes the last four bytes that will eventually be XORd
- Because we are adding up 0x1b bytes then multiplying by (in this case) 4, the maximum number is not very large compared to the 32 bit number comprising of the last 4 bytes.
I picked the number 0xFACE1C24 to go into the file, which when XORd with 0xFACE0FB0 leaves us with 0x1394, a number that is evenly divisble by 4, resulting in 0x4e5.
We already have a part of the sum in the file : 0xFA + 0xCE + 0x1C + 0x24 = 0x208
This leaves us with 0x2dd to spread over the remaining 0x17 bytes, which works out as 0x16 * 0x1f bytes and the last byte of 0x33.
The finished license file looks like this and passes the check.
DEC0ADDE041F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F33241CCEFA
Tuesday
The first thing Tuesday does is copy the username into an area of memory that is 0x20 bytes long. If the username is not long enough it is repeated until all the memory is used up (IDIV
is being used for the remainder here)
This next bit of code is quite interesting, it uses the special CPUID
instruction that writes various information into the registers about the current processor. The code then swaps, xors and generally mixes together a bunch of them to produce a magic number.
The following loop then cycles through the memory buffer from earlier, XORing each 4 bytes with the magic number, leaving an encrypted form of the repeated username.
The first lines here check that the first byte of the serial is 0x2D303154
which if you look at in ASCII with the correct endian-ness is T10-
. The rest of the code is a couple of loops worth of number crunching to generate the final result. You can see it replicated here in this beautiful keygen:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
#define cpuid__asm __emit 0fh __asm __emit 0a2h
typedef bool (__stdcall*func)(const char*, const char*);
void tuesday(char* name)
{
char buff[0x20];
int id = 0; // cpuid magic
__asm {
push eax
push ebx
push ecx
push edx
MOV EAX, 0
cpuid
XOR EBX, EDX
BSWAP ECX
XOR EBX, ECX
PUSH EBX
MOV EAX, 1
cpuid
BSWAP EAX
XOR EDX, ECX
XOR EAX, EDX
POP EBX
XOR EAX, EBX
XCHG AL, AH
MOV id, EAX
pop edx
pop ecx
pop ebx
pop eax
};
int len = strlen(name);
for (int i = 0; i < 0x20; i++)
{
buff[i] = name[i % len];
}
for (int i = 0; i < 0x20; i += 4)
{
int* x = (int*)(&buff[i]);
*x ^= id;
}
unsigned int magic = 0xb00b;
int a = 0;
for (int i = 0; i < 0x20; i++)
{
a = a & 0xFFFFFF00;
a |= buff[i];
a *= len;
magic ^= a;
magic <<= 4;
magic &= 0xFFFFFFFF;
}
unsigned int bak = magic;
bak >>= 0x10;
magic ^= bak;
magic &= 0xFFFF;
int total = 0;
for (int i = 0; i < 0x4; i++)
{
bak = magic;
magic &= 0xf;
magic += 0x30;
if (magic > 0x39)
{
magic += 0x7;
}
total <<= 8;
total += magic;
magic = bak;
magic >>= 4;
}
char serial[5];
serial[4] = '\0';
int* ser = (int*)&serial;
*ser = total;
std::cout << "serial for " << name << " on Tuesday is T10-" << serial;
}
|
Wednesday
The first part of this calculates a number based on the whole serial.
Here’s an approximation
1 2 3 4 5 6 7 8 9 |
int len = strlen(serial);
unsigned int sertot = 0;
for (int i = 0; i < len; i++)
{
char c = serial[i];
c -= 0x37;
sertot <<= 4;
sertot += c;
}
|
I left out some of the conditional checks since they are seemingly irrelevant for me to generate a correct key.
This part calculates a number from the user name. There’s a lot of instructions here that use the lower parts of the registers which are a bit of a pain to emulate in a high level language like C. An important thing to notice here is that it only uses the first 4 chracters of the username, and ignores the rest. Eventually, it ends up with a 16 bit number that it copies into the upper part of the register as well, resulting in a 32 bit number consisting of two identical 16 bit numbers.
An ugly approximation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
unsigned int res = name[0] + name[1];
res *= res;
unsigned char a = res << 4;
res &= 0xFFFFFF00;
res |= a;
a = (res & 0xFF) + name[2];
a ^= name[3];
res &= 0xFFFFFF00;
res |= a;
res = (res & 0xff) * ((res & 0x0000ff00) >> 8);
a = res << 4;
res &= 0xFFFFFF00;
res |= a;
a = (res & 0xff) + name[2] + name[0];
res &= 0xFFFFFF00;
res |= a;
unsigned int temp = res;
res = (temp << 16) | 0x0000;
res ^= temp; // our final value.
|
The resulting numbers from the two stages must be equal. Since the first part uses each character of the serial in an add-shift fashion, the actual characters don’t matter, only that they add up to the correct value. Therefore we can take the result of the username calculation and reverse it out into a serial that will add up to the correct number for us. Since we know the 16 bit number is replicated, we can take the first 4 characters and write out an 8 character serial:
1 2 3 4 5 6 7 8 9 |
char newSer[8];
for (int i = 0; i < 4; i++)
{
char c = res & 0xf;
c += 0x37;
res >>= 4;
newSer[3-i] = c;
newSer[(3-i)+4] = c;
}
|
Feeding the original code a username of pezip
generates a value of 0x441a441a
. Putting this through the above function yields a serial of ;;8A;;8A
that can be used to successfully pass the check.
Thursday
Thursday has a lot of different routines and stuff going on, starting with a check that the serial is 0x20 bytes long. After a bunch of poking and tracing around, I discovered a “smoking gun”
The constants here are commonly associated with MD5/SHA1 hashing. And so it would transpire that Thursday’s serial number is a direct MD5 hash of the username.
Friday
Friday is the biggest one yet so I will not show so much of the code. A bunch of investigation revealed the following:
- A hashing function produces a number from the name
- The serial is expected to be in a particular format;
- NNNNNNNN-AAAA-BBBB-CCCC-DDDD- with the hyphens, where;
- N should match the output from the name hashing function (with some logistical changes)
- A and B form two numbers for a particular check
In order to process the serial string input, the following function is used - that also appeared in Thursday’s challenge.
There’s some trickery here using the carry flags and an odd way of detecting the end of the string. I eventually distilled it to the following code (it’s not the same, but the essence of what it is doing if given the correct input):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
char buff[0x18];
int index = 0;
int dindex = 0;
while (true)
{
char c = serial[index];
c < 0x40 ? c -= 0x30 : c -= 0x37;
c <<= 0x4;
char d = serial[index + 1];
d < 0x40 ? d -= 0x30 : d -= 0x37;
buff[dindex++] = c + (d & 0xf);
index += 2;
if (serial[index] == 0)
{
break;
}
}
|
Even with this it still took me a good while to realise that all it was doing was parsing hexadecimal numbers from ascii. Sometimes you can’t see the wood through the trees.
The checks for A and B are as follows, and do not rely on the name in any way.
The numbers are checked to be within a certain range, then XORing them together must match the result of multiplying them together and masking to 0x7FFF.
With a little thought and experimentation, 0x1000 for both values satisfies this check.
The next two numbers undergo a similar prodedure, except this time (within the same bounds) the numbers must satisfy (n ^^^ n2) = (( n * n2) >>> 0xA)
. I am sure there’s a nice mathematical way of deriving the possible solutions, however I simply brute forced a bunch of them and picked one.
The last part of this code checks that the hyphens appear in the correct places. A valid serial for user12345
is 0?3;02:1-1000-1000-07a3-0820
Saturday
Saturday looked quite daunting with lots of subroutine calls. After a lot of inspection (several hours!), it seemed to be doing an awful lot of work, with several hashing / crunching procedures and a lot of code. It was too difficult for me to work out exactly what each part was doing since I didn’t recognise all the algorithms, so instead I decided to cheat. Based on the observation that after all the crunching had completed, the program simply checks the result against the serial you give it after running it through the ascii->hex function from earlier:
You can see here it checks the buffers against each other for 0x10 characters using repe cmpsb
I made a copy of the DLL and changed the function, so that instead of checking the crunched result against the serial, it returns the crunched result instead:
Now in my keygen program, I can call this function and have it generate the correct value for me, process it through a function that performs the inverse of the ascii->hex function, leaving us with the correct serial. For the username user12345
the serial is FB368210295047499E06AC2352C62885
Conclusion
This was a fun crackme, and I am pleased I could provide a solution of some description for each puzzle. There were no anti-debugging techniques present in this one, instead focusing more on hashing things in various ways. One puzzle remains, however - if you look at the C# code you will see it MD5 hashes the name and tests it against two hard-coded hashes. I have not tried to work out what these could be yet, maybe there are some clues hanging around in the .exe and .dll somewhere …